{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "from torch.autograd import Variable\n",
    "from torch import optim\n",
    "from torch.utils.data import DataLoader\n",
    "from torchvision import datasets, transforms\n",
    "import numpy as np\n",
    "#from ignite.engine import Events, create_supervised_trainer, create_supervised_evaluator\n",
    "#from ignite.metrics import Accuracy, Loss\n",
    "#from tqdm import tqdm"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据增强"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 训练集数据增强\n",
    "包含各种变换, 增强其泛化能力"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 训练集数据增强\n",
    "train_transform = transforms.Compose(\n",
    "    [\n",
    "        transforms.RandomHorizontalFlip(0.5),\n",
    "        transforms.RandomCrop(32, padding=4),\n",
    "        transforms.ToTensor(),\n",
    "        transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 测试集数据只做标准化"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 测试集数据增强\n",
    "test_transform = transforms.Compose(\n",
    "    [\n",
    "        transforms.ToTensor(),\n",
    "        transforms.Normalize([0.5 ,0.5, 0.5], [0.5, 0.5, 0.5])\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义网络结构"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 定义 ResNet 基本模块"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def conv3x3(in_channels, out_channels, stride=1):\n",
    "    \"\"\"\n",
    "        基本 3*3 卷积层\n",
    "        \n",
    "        params\n",
    "        ------\n",
    "        in_channels : int\n",
    "        out_channels: int\n",
    "        stride : int\n",
    "            滑动步长\n",
    "    \"\"\"\n",
    "    return nn.Conv2d(in_channels, out_channels, kernel_size=(3,3), stride=stride, padding=1, bias=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class ResidualBlock(nn.Module):\n",
    "    \"\"\"\n",
    "        残差块\n",
    "    \"\"\"\n",
    "    def __init__(self, in_channels, out_channels, stride=1, downsample=None):\n",
    "        super(ResidualBlock, self).__init__()\n",
    "        # 卷积层\n",
    "        self.conv1 = conv3x3(in_channels, out_channels, stride)\n",
    "        # 批标准化\n",
    "        self.bn1 = nn.BatchNorm2d(out_channels)\n",
    "        # ReLU 激活函数\n",
    "        self.relu = nn.ReLU(True)\n",
    "        \n",
    "        self.conv2 = conv3x3(out_channels, out_channels)\n",
    "        self.bn2 = nn.BatchNorm2d(out_channels)\n",
    "        # downsample 用来改变输出的通道数 (通过1*1卷积实现)\n",
    "        self.downsample = downsample\n",
    "        \n",
    "        \n",
    "    def forward(self, x):\n",
    "        residual = x\n",
    "        out = self.conv1(x)\n",
    "        out = self.bn1(out)\n",
    "        out = self.relu(out)\n",
    "        out = self.conv2(out)\n",
    "        out = self.bn2(out)\n",
    "        if self.downsample:\n",
    "            residual = self.downsample(x)\n",
    "        # 激活函数前, 相加, 如果通道数不相等, 在上一步进行downsample\n",
    "        out += residual\n",
    "        out = self.relu(out)\n",
    "        return out"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 定义ResNet"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class ResNet(nn.Module):\n",
    "    def __init__(self, block, layers, num_classes=10):\n",
    "        \"\"\"\n",
    "            ResNet 残差网络\n",
    "            \n",
    "            params\n",
    "            ------\n",
    "            block : ResidualBlock\n",
    "                使用的块的类别\n",
    "            layers : List[int]\n",
    "            num_classes : int\n",
    "        \"\"\"\n",
    "        super(ResNet, self).__init__()\n",
    "        self.in_channels = 16\n",
    "        \n",
    "        self.conv = conv3x3(in_channels=3, out_channels=16)\n",
    "        self.bn = nn.BatchNorm2d(16)\n",
    "        self.relu = nn.ReLU(True)\n",
    "        # 动态定义网络\n",
    "        self.layer1 = self.make_layer(block, 16, layers[0])\n",
    "        self.layer2 = self.make_layer(block, 32, layers[1], 2)\n",
    "        self.layer3 = self.make_layer(block, 64, layers[2], 2)\n",
    "        \n",
    "        # 平均池化\n",
    "        self.avg_pool = nn.AvgPool2d(kernel_size=(8,8))\n",
    "        self.drop_out = nn.Dropout(0.5)\n",
    "        self.fc = nn.Linear(in_features=64, out_features=num_classes)\n",
    "        \n",
    "        \n",
    "    def make_layer(self, block, out_channels, blocks, stride=1):\n",
    "        \"\"\"\n",
    "            \n",
    "            params\n",
    "            ------\n",
    "            block : ResidualBlock\n",
    "                使用的块的类别\n",
    "            out_channels : int\n",
    "                输出通道数\n",
    "            blocks : int\n",
    "                该层残差块的总数\n",
    "            stride : int\n",
    "                滑动步长\n",
    "        \"\"\"\n",
    "        downsample = None\n",
    "        # 输出通道数不等于输出通道数, 则下采样\n",
    "        if (stride != 1) or (self.in_channels != out_channels):\n",
    "            downsample = nn.Sequential(\n",
    "                conv3x3(self.in_channels, out_channels, stride=stride),\n",
    "                nn.BatchNorm2d(out_channels)\n",
    "            )\n",
    "            \n",
    "        layers = []\n",
    "        layers.append(\n",
    "            block(self.in_channels, out_channels, stride, downsample)\n",
    "        )\n",
    "        self.in_channels = out_channels\n",
    "        for i in range(1, blocks):\n",
    "            layers.append(block(out_channels, out_channels))\n",
    "            \n",
    "        return nn.Sequential(*layers)\n",
    "    \n",
    "    \n",
    "    def forward(self, x):\n",
    "        out = self.conv(x)\n",
    "        out = self.bn(out)\n",
    "        out = self.relu(out)\n",
    "        out = self.layer1(out)\n",
    "        out = self.layer2(out)\n",
    "        out = self.layer3(out)\n",
    "        out = self.avg_pool(out)\n",
    "        # 展平, 输入到全连接层\n",
    "        out = out.view(out.size(0), -1)\n",
    "        out = self.fc(out)\n",
    "        return out"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义超参数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 线程数\n",
    "WORKERS = 4\n",
    "BATCH_SIZE = 256\n",
    "# 学习率\n",
    "LR = 0.001\n",
    "# 训练轮次\n",
    "NUM_EPOCHS = 20"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "train: 40000  val:10000  test:10000\n"
     ]
    }
   ],
   "source": [
    "train_data = datasets.CIFAR10(root=\"./data\", train=True, download=False, transform=train_transform)\n",
    "# 划分训练集和验证集\n",
    "train_data, val_data = torch.utils.data.random_split(train_data, [40000, 10000])\n",
    "\n",
    "# 测试集\n",
    "test_data = datasets.CIFAR10(root=\"./data\", train=False, download=False, transform=test_transform)\n",
    "\n",
    "print(f\"train: {len(train_data)}  val:{len(val_data)}  test:{len(test_data)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 损失函数和优化器"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# ResNet 残差网络, 分别采用2, 2, 2 个残差块\n",
    "model = ResNet(ResidualBlock, [2,2,0])\n",
    "if torch.cuda.is_available():\n",
    "    model = model.cuda()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 多分类损失函数 交叉熵\n",
    "criterion = nn.CrossEntropyLoss().cuda()\n",
    "# Adam 优化器, L2 正则化项权重 0.01\n",
    "optimizer = optim.Adam(model.parameters(), lr=LR, weight_decay=0.01)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "## 训练模型"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### checkpoint 机制"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def save_checkpoint(state, epoch, NUM_EPOCHS, is_best, filename='./model/checkpoint.pth'):\n",
    "    \"\"\"\n",
    "        保存最优的checkpoint\n",
    "    \"\"\"\n",
    "    if is_best:\n",
    "        print (f\"{epoch+1}/{NUM_EPOCHS}  => Saving a new best\")\n",
    "        torch.save(state, filename) # save checkpoint\n",
    "    else:\n",
    "        print (\"=> Validation Accuracy did not improve\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 训练和验证"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1/20  => Saving a new best\n",
      "1/20 loss:0.006423  acc:0.3937  val_loss: 0.006282  val_acc:0.4122\n",
      "2/20  => Saving a new best\n",
      "2/20 loss:0.004943  acc:0.5507  val_loss: 0.005052  val_acc:0.5381\n",
      "3/20 loss:0.004264  acc:0.6193  val_loss: 0.005381  val_acc:0.5035\n",
      "4/20 loss:0.003939  acc:0.6518  val_loss: 0.005410  val_acc:0.5249\n",
      "5/20  => Saving a new best\n",
      "5/20 loss:0.003753  acc:0.6684  val_loss: 0.004470  val_acc:0.6050\n",
      "6/20 loss:0.003552  acc:0.6920  val_loss: 0.004928  val_acc:0.5623\n",
      "7/20  => Saving a new best\n",
      "7/20 loss:0.003359  acc:0.7104  val_loss: 0.004067  val_acc:0.6378\n",
      "8/20 loss:0.003224  acc:0.7235  val_loss: 0.004258  val_acc:0.6274\n",
      "9/20 loss:0.003087  acc:0.7375  val_loss: 0.004534  val_acc:0.6242\n",
      "10/20  => Saving a new best\n",
      "10/20 loss:0.003020  acc:0.7429  val_loss: 0.003846  val_acc:0.6641\n",
      "11/20  => Saving a new best\n",
      "11/20 loss:0.002886  acc:0.7579  val_loss: 0.003559  val_acc:0.6858\n",
      "12/20 loss:0.002832  acc:0.7629  val_loss: 0.003818  val_acc:0.6663\n",
      "13/20 loss:0.002777  acc:0.7690  val_loss: 0.004384  val_acc:0.6225\n",
      "14/20 loss:0.002705  acc:0.7752  val_loss: 0.004302  val_acc:0.6237\n",
      "15/20 loss:0.002690  acc:0.7772  val_loss: 0.004719  val_acc:0.6039\n",
      "16/20 loss:0.002640  acc:0.7825  val_loss: 0.004334  val_acc:0.6558\n",
      "17/20  => Saving a new best\n",
      "17/20 loss:0.002605  acc:0.7853  val_loss: 0.003487  val_acc:0.6998\n",
      "18/20 loss:0.002575  acc:0.7872  val_loss: 0.003618  val_acc:0.6937\n",
      "19/20 loss:0.002543  acc:0.7901  val_loss: 0.003904  val_acc:0.6620\n",
      "20/20 loss:0.002544  acc:0.7920  val_loss: 0.003850  val_acc:0.6634\n"
     ]
    }
   ],
   "source": [
    "train_acc_list = []\n",
    "train_loss_list = []\n",
    "val_acc_list = []\n",
    "val_loss_list = []\n",
    "\n",
    "# 当前最佳表现, 用于保存checkpoint\n",
    "best_val_acc = 0.0\n",
    "best_val_loss = 10000.0\n",
    "\n",
    "for epoch in range(NUM_EPOCHS):\n",
    "    # 训练\n",
    "    model.train()\n",
    "    correct = torch.zeros(1).squeeze().cuda()\n",
    "    total = torch.zeros(1).squeeze().cuda()\n",
    "    train_loss = 0.0\n",
    "    correct = 0.0\n",
    "    \n",
    "    train_loader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, num_workers=WORKERS, pin_memory=True)\n",
    "    val_loader = DataLoader(val_data, batch_size=BATCH_SIZE, shuffle=True, num_workers=WORKERS, pin_memory=True)\n",
    "    \n",
    "    for batch_X, batch_y in train_loader:\n",
    "        # GPU 可用\n",
    "        if torch.cuda.is_available():\n",
    "            batch_X, batch_y = Variable(batch_X).cuda(), Variable(batch_y).cuda()\n",
    "        else:\n",
    "            batch_X, batch_y = Variable(batch_X), Variable(batch_y)\n",
    "            \n",
    "        # 前向传播, 计算损失\n",
    "        out = model(batch_X)\n",
    "        loss = criterion(out, batch_y)\n",
    "        train_loss += loss.item()\n",
    "        # train_loss += loss.data\n",
    "\n",
    "        # prediction = torch.argmax(out, 1)\n",
    "        # correct += (prediction == batch_y).sum().float()\n",
    "        # total += len(batch_y)\n",
    "\n",
    "        # 清除累计的梯度, 反向传播\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        \n",
    "        # 计算预测结果的类别\n",
    "        prediction = out.data.max(1)[1]\n",
    "        correct += float(prediction.eq(batch_y.data).sum().data)\n",
    "\n",
    "    # 本轮的训练损失\n",
    "    # train_loss = train_loss / len(train_loader)\n",
    "    train_loss /= len(train_loader.dataset)\n",
    "    \n",
    "    # 本轮的训练精度\n",
    "    # train_acc = (correct/total).cpu().detach().data.numpy()\n",
    "    train_acc = correct/len(train_loader.dataset)\n",
    "    \n",
    "    # correct = torch.zeros(1).squeeze().cuda()\n",
    "    # total = torch.zeros(1).squeeze().cuda()\n",
    "    \n",
    "    # 在验证集上验证\n",
    "    model.eval()\n",
    "    val_loss = 0.0\n",
    "    correct = 0.0\n",
    "    \n",
    "    for batch_X, batch_y in val_loader:\n",
    "        # GPU 可用\n",
    "        if torch.cuda.is_available():\n",
    "            batch_X, batch_y = Variable(batch_X).cuda(), Variable(batch_y).cuda()\n",
    "        else:\n",
    "            batch_X, batch_y = Variable(batch_X), Variable(batch_y)\n",
    "            \n",
    "        out = model(batch_X)\n",
    "        #loss = criterion(out, batch_y)\n",
    "        val_loss += criterion(out, batch_y).item()\n",
    "        # val_loss += loss.data\n",
    "\n",
    "        #prediction = torch.argmax(out, 1)\n",
    "        #correct += (prediction == batch_y).sum().float()\n",
    "        #total += len(batch_y)\n",
    "        prediction = out.data.max(1)[1]\n",
    "        \n",
    "        correct += float(prediction.eq(batch_y.data).sum().data)\n",
    "        \n",
    "    # 本轮的损失\n",
    "    # val_loss = train_loss / len(train_loader)\n",
    "    val_loss /= len(val_loader.dataset)\n",
    "    val_acc = correct/len(val_loader.dataset)\n",
    "    \n",
    "    # 本轮验证是否更优， 监控指标为 val_acc\n",
    "    is_better = (val_acc > best_val_acc) and (val_loss <= best_val_loss)\n",
    "    \n",
    "    if is_better:\n",
    "        best_val_acc = val_acc\n",
    "        best_val_loss = val_loss\n",
    "    \n",
    "        save_checkpoint(state=model.state_dict(), epoch=epoch, NUM_EPOCHS=NUM_EPOCHS, is_best=True)\n",
    "\n",
    "    \n",
    "    # 本轮的精度\n",
    "    # val_acc = (correct/total).cpu().detach().data.numpy()\n",
    "    print(f\"{epoch+1}/{NUM_EPOCHS} loss:{train_loss:2.6f}  acc:{train_acc:1.4f}  val_loss: {val_loss:2.6f}  val_acc:{val_acc:1.4f}\")\n",
    "    # print(f\"{epoch+1}/{NUM_EPOCHS}  loss:{train_loss:1.6f}  acc:{float(train_acc):2.4f}  val_loss:{val_loss:1.6f}  val_acc:{float(val_acc):2.4f}\")\n",
    "    \n",
    "    train_acc_list.append(train_acc)\n",
    "    train_loss_list.append(train_loss)\n",
    "    val_acc_list.append(val_acc)\n",
    "    val_loss_list.append(val_loss)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 绘制训练曲线"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "\n",
    "\n",
    "epochs = range(1, len(train_acc_list) + 1)\n",
    "\n",
    "plt.plot(epochs, train_acc_list, 'bo', label = 'Trianing acc')\n",
    "plt.plot(epochs, val_acc_list, 'b', label = 'Validation acc')\n",
    "plt.title('Training and validation acc')\n",
    "plt.legend()\n",
    "\n",
    "plt.figure()\n",
    "\n",
    "plt.plot(epochs, train_loss_list, 'bo', label = 'Taining loss')\n",
    "plt.plot(epochs, val_loss_list, 'b', label = 'Validation loss')\n",
    "plt.title('Training and validation loss')\n",
    "plt.legend()\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试集上评估模型"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 读入 checkpoint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# 读入checkpoint\n",
    "if torch.cuda.is_available():\n",
    "    checkpoint = torch.load(\"./model/checkpoint.pth\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet(\n",
       "  (conv): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "  (bn): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "  (relu): ReLU(inplace=True)\n",
       "  (layer1): Sequential(\n",
       "    (0): ResidualBlock(\n",
       "      (conv1): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    )\n",
       "    (1): ResidualBlock(\n",
       "      (conv1): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    )\n",
       "  )\n",
       "  (layer2): Sequential(\n",
       "    (0): ResidualBlock(\n",
       "      (conv1): Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (downsample): Sequential(\n",
       "        (0): Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      )\n",
       "    )\n",
       "    (1): ResidualBlock(\n",
       "      (conv1): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    )\n",
       "  )\n",
       "  (layer3): Sequential(\n",
       "    (0): ResidualBlock(\n",
       "      (conv1): Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (downsample): Sequential(\n",
       "        (0): Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      )\n",
       "    )\n",
       "  )\n",
       "  (avg_pool): AvgPool2d(kernel_size=(8, 8), stride=(8, 8), padding=0)\n",
       "  (drop_out): Dropout(p=0.5, inplace=False)\n",
       "  (fc): Linear(in_features=64, out_features=10, bias=True)\n",
       ")"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 模型读入最优权重\n",
    "model.load_state_dict(checkpoint)\n",
    "model.eval()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 在测试集上测试"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test_loss: 0.003328  test_acc:0.7161\n"
     ]
    }
   ],
   "source": [
    "test_loader = DataLoader(test_data, batch_size=BATCH_SIZE, shuffle=False, num_workers=WORKERS, pin_memory=True)\n",
    "\n",
    "test_loss = 0.0\n",
    "correct = 0.0\n",
    "    \n",
    "for batch_X, batch_y in test_loader:\n",
    "    # GPU 可用\n",
    "    if torch.cuda.is_available():\n",
    "        batch_X, batch_y = Variable(batch_X).cuda(), Variable(batch_y).cuda()\n",
    "    else:\n",
    "        batch_X, batch_y = Variable(batch_X), Variable(batch_y)\n",
    "    out = model(batch_X)\n",
    "    #loss = criterion(out, batch_y)\n",
    "    test_loss += criterion(out, batch_y).item()\n",
    "    # val_loss += loss.data\n",
    "\n",
    "    #prediction = torch.argmax(out, 1)\n",
    "    #correct += (prediction == batch_y).sum().float()\n",
    "    #total += len(batch_y)\n",
    "    prediction = out.data.max(1)[1]\n",
    "\n",
    "    correct += float(prediction.eq(batch_y.data).sum().data)\n",
    "\n",
    "# 本轮的损失\n",
    "# val_loss = train_loss / len(train_loader)\n",
    "test_loss /= len(test_loader.dataset)\n",
    "# 本轮的精度\n",
    "# val_acc = (correct/total).cpu().detach().data.numpy()\n",
    "print(f\"test_loss: {test_loss:2.6f}  test_acc:{correct/len(test_loader.dataset)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "## 保存模型 （已有checkpoint, 此处代码搁置）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Anaconda3\\envs\\torch\\lib\\site-packages\\torch\\serialization.py:256: UserWarning: Couldn't retrieve source code for container of type ResNet. It won't be checked for correctness upon loading.\n",
      "  \"type \" + obj.__name__ + \". It won't be checked \"\n",
      "C:\\Anaconda3\\envs\\torch\\lib\\site-packages\\torch\\serialization.py:256: UserWarning: Couldn't retrieve source code for container of type ResidualBlock. It won't be checked for correctness upon loading.\n",
      "  \"type \" + obj.__name__ + \". It won't be checked \"\n"
     ]
    }
   ],
   "source": [
    "torch.save(model, \"./model/resnet_cifar10.pth\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 以下代码搁置"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {
    "code_folding": [],
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# def get_data_loaders(train_batch_size, val_batch_size, data_transform):\n",
    "#     \"\"\"\n",
    "#         训练集和验证集的迭代器\n",
    "        \n",
    "#         params\n",
    "#         ------\n",
    "#         train_batch_size : int\n",
    "#             训练集 batch_size\n",
    "#         train_transform : transforms.Compose\n",
    "#             数据增强, 训练集和验证集使用相同的数据增强\n",
    "#     \"\"\"\n",
    "\n",
    "#     train_loader = DataLoader(MNIST(download=False, root=\"./data\", transform=data_transform, train=True),\n",
    "#                               batch_size=train_batch_size, shuffle=True)\n",
    "\n",
    "#     val_loader = DataLoader(MNIST(download=False, root=\"./data\", transform=data_transform, train=False),\n",
    "#                             batch_size=val_batch_size, shuffle=False)\n",
    "#     return train_loader, val_loader"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet(\n",
       "  (conv): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "  (bn): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "  (relu): ReLU(inplace=True)\n",
       "  (layer1): Sequential(\n",
       "    (0): ResidualBlock(\n",
       "      (conv1): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    )\n",
       "    (1): ResidualBlock(\n",
       "      (conv1): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    )\n",
       "  )\n",
       "  (layer2): Sequential(\n",
       "    (0): ResidualBlock(\n",
       "      (conv1): Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (downsample): Sequential(\n",
       "        (0): Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      )\n",
       "    )\n",
       "    (1): ResidualBlock(\n",
       "      (conv1): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    )\n",
       "  )\n",
       "  (layer3): Sequential(\n",
       "    (0): ResidualBlock(\n",
       "      (conv1): Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (relu): ReLU(inplace=True)\n",
       "      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      (downsample): Sequential(\n",
       "        (0): Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "      )\n",
       "    )\n",
       "  )\n",
       "  (avg_pool): AvgPool2d(kernel_size=(8, 8), stride=(8, 8), padding=0)\n",
       "  (drop_out): Dropout(p=0.5, inplace=False)\n",
       "  (fc): Linear(in_features=64, out_features=10, bias=True)\n",
       ")"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model.eval()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  },
  "latex_envs": {
   "LaTeX_envs_menu_present": true,
   "autoclose": false,
   "autocomplete": true,
   "bibliofile": "biblio.bib",
   "cite_by": "apalike",
   "current_citInitial": 1,
   "eqLabelWithNumbers": true,
   "eqNumInitial": 1,
   "hotkeys": {
    "equation": "Ctrl-E",
    "itemize": "Ctrl-I"
   },
   "labels_anchors": false,
   "latex_user_defs": false,
   "report_style_numbering": false,
   "user_envs_cfg": false
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "position": {
    "height": "430px",
    "left": "1466.28px",
    "right": "20px",
    "top": "63.9757px",
    "width": "587px"
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
